<?php
/**
 * OpenID 2.0 Authentication for PHP
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * Copyright (c) 2006-2007 Pádraic Brady <padraic.brady@yahoo.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * The name of the author may not be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category Authentication
 * @package  OpenID
 * @author   Pádraic Brady <padraic.brady@yahoo.com>
 * @license  http://opensource.org/licenses/bsd-license.php New BSD License
 * @link     http://pearopenid.googlecode.com/
 */

/** OpenID_Exception */
require_once 'OpenID/Exception.php';

/** Validate */
require_once 'Validate.php';

/**
 * OpenID ancestor class acting as a version and constant holder for the
 * Consumer and Provider descendants implementing the 1.1 and 2.0
 * OpenID Authentication specifications.
 *
 * Simple example authentication script:
 *
 *      require_once 'OpenID/Consumer.php';
 *      require_once 'OpenID/Store/File.php';
 *      
 *      $store = new OpenID_Store_File(dirname(__FILE__)); // use current
 *      working dir to cache association data used for verifying response 
 *      signatures
 *      $test = new OpenID_Consumer($store);
 *      
 *      // Catch an authentication response to this script
 *      if (isset($_GET) && !empty($_GET)) {
 *          $result = $test->finish($_GET);
 *          if ($result->getResult() !== OpenID::OPENID_RESPONSE_SUCCESS) {
 *              exit('Result was:' . $result->getResult());
 *                        // "parse error" is often a library screw-up ; )
 *          }
 *          echo 'Authenticated with User OpenID: ',
 *              $result->get('openid.claimed_id'),
 *              ' The OpenID Provider knows you as ',
 *              $result->get('openid.identity'), '<br/>';
 *              exit(0);
 *      }
 *      
 *      OpenID::setVersion(2.0);
 *      $authRedirect = $test->start('padraic.myopenid.com');
 *          // last param should be the the base url
 *      $authRedirect->redirect(
 *          'http://localhost/path/to/this/file',
 *          'http://localhost'
 *      );
 *
 * @category Authentication
 * @package  OpenID
 * @author   "Pádraic Brad <padraic.brady@yahoo.com>
 * @license  http://opensource.org/licenses/bsd-license.php New BSD License
 * @link     http://pearopenid.googlecode.com/
 */
class OpenID
{

    /**
     * Static version value; default is 2.0. If a 2.0 process is failing, we
     * may fall back to 1.1 during discovery stage. This may also be changed
     * while validating responses when the OP sends a 1.1 namespace value
     * indicating the response should be interpreted under the 1.1
     * backwards compatibility rules.
     *
     * @var float
     */
    protected static $version = 2.0;

    /**
     * The Openid Extension Namespaces supported by the current
     * instance. These can be extended by adding namespaces using
     * the static OpenID::addExtensionNamespace() method.
     *
     * @see addExtensionNamespace()
     * @var array
     */
    protected static $extensionNamespaces = array(
        'sreg' => 'http://openid.net/extensions/sreg/1.1',
        'ax'   => 'http://openid.net/srv/ax/1.0',
        'pape' => 'http://specs.openid.net/extensions/pape/1.0'
    );

    /**
     * Maintains the current association type
     *
     * @var string
     */
    protected static $assocType = 'HMAC-SHA256';

    /**
     * Maintains the current session type
     *
     * @var string
     */
    protected static $sessionType = 'DH-SHA256';

    /**
     * Constants:
     * OpenID 2.0 Authentication Response Codes
     */
    const OPENID_RESPONSE_SUCCESS      = 'success';
    const OPENID_RESPONSE_CANCEL       = 'cancel';
    const OPENID_RESPONSE_FAILURE      = 'failure';
    const OPENID_RESPONSE_SETUP_NEEDED = 'setup needed';
    const OPENID_RESPONSE_PARSE_ERROR  = 'parse error';

    /**
     * Constants:
     * OpenID 1.0 XML Namespace for Yadis XRD Documents
     */
    const OPENID_XML_NAMESPACE = 'http://openid.net/xmlns/1.0';

    /**
     * Constants:
     * OpenID 2.0/1.1 HTTP Request/Response Namespace Keys
     */
    const OPENID_2_0_NAMESPACE = 'http://specs.openid.net/auth/2.0';
    const OPENID_1_1_NAMESPACE = 'http://openid.net/signon/1.1';
    const OPENID_1_0_NAMESPACE = 'http://openid.net/signon/1.0';

    /**
     * Constants:
     * OpenID 2.0 Yadis XRD Service Types
     */
    const OPENID_2_0_SERVICE_SERVER_TYPE = 'http://specs.openid.net/auth/2.0/server';
    const OPENID_2_0_SERVICE_SIGNON_TYPE = 'http://specs.openid.net/auth/2.0/signon';
    const OPENID_1_2_SERVICE_SIGNON_TYPE = 'http://openid.net/signon/1.2';
    const OPENID_1_1_SERVICE_SIGNON_TYPE = 'http://openid.net/signon/1.1';
    const OPENID_1_0_SERVICE_SIGNON_TYPE = 'http://openid.net/signon/1.0';

    /**
     * Constants:
     * Supported Association Hash Algorithms (preferred)
     */
    const OPENID_1_1_HASH_ALGORITHM = 'SHA1';
    const OPENID_2_0_HASH_ALGORITHM = 'SHA256';

    /**
     * Constants:
     * If you don't use SSL, do not even think about not using Encryption!
     * The second set are for use in a non-statefull environment, i.e. where
     * you cannot store association data to Database/File or where PHP Sessions
     * are not available. Obviously this is not great either.
     *
     * Avoid both like the plague unless you have a really good reason!
     */
    const OPENID_NO_ENCRYPTION = 'no-encryption';
    const OPENID_STATELESS     = 'stateless';
    const OPENID_STATEFULL     = 'statefull';


    /**
     * Constants:
     * OpenID PHP Session Namespace
     */
    const OPENID_PHP_SESSION_NAMESPACE = 'OPENID_SESSION';

    /**
     * Constants:
     * Diffie-Hellman Constants
     */
    const OPENID_DIFFIEHELLMAN_DEFAULT_PRIME = '155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443';

    const OPENID_DIFFIEHELLMAN_DEFAULT_GENERATOR = '2';

    /**
     * Base64 Encoded BTWOC Representations of the default DH values
     * A wee bit of preemptive caching
     */
    const OPENID_DIFFIEHELLMAN_DEFAULT_PRIME_BASE64 = 'ANz5OguIOXLsDhmYmsWizjEOHTdxfo2Vcbt2I3MYZuYe91ouJ4mLBX+YkcLiemOcPym2CBRYHNOyyjmG0mg3BVd9RcLn5S3IHHoXGHblzqdLFEi/368Ygo79JRnxTkXjgmY0rxlJ5bU1zIKaSDuKdiI+XUkKJX8Fvf8W8vsixYOr';

    const OPENID_DIFFIEHELLMAN_DEFAULT_GENERATOR_BASE64 = 'Ag==';

    /**
     * Constants:
     * Prefix for keys in Request Parameters
     */
    const OPENID_ARGUMENT_KEY_PREFIX = 'openid';

    /**
     * Mode; either the client supports storing state between
     * sessions (e.g. database), or it doesn't. In PHP, we really
     * should never use OpenID in stateless mode unless strictly
     * for testing purposes.
     *
     * @var string
     */
    protected static $mode = OpenID::OPENID_STATEFULL;

    /**
     * Set the current OpenID specification version we are proceeding under.
     * This can be reset during service/OP discovery in order to force a
     * fallback position to 1.1 (for those providers/aliases not yet updating
     * to 2.0 for example).
     *
     * @param float $version OpenID Version
     *
     * @return void
     */
    public static function setVersion($version)
    {
        if (!in_array($version, array(1.1, 2.0))) {
            throw new OpenID_Exception('Not a valid OpenID Specification version');
        }
        $hash = ($version == 1.1) ? 'sha1' : 'sha256';
        self::setAssocType($hash);
        self::setSessionType($hash);
        self::$version = $version;
    }

    /**
     * Return the current operating version
     *
     * @return float
     */
    public static function getVersion()
    {
        return self::$version;
    }

    /**
     * Set additional Extension namespaces so responses
     * may be parsed accordingly.
     *
     * @param string $namespace Namespace
     * @param string $uri       URI
     *
     * @throws OpenID_Exception on invalid URI
     * @return bool true on success, false if the namespace was already set
     */
    public static function addExtensionNamespace($namespace, $uri)
    {
        if (!in_array($namespace, self::$extensionNamespaces)) {
            if (!Validate::uri($uri)) {
                throw new OpenID_Exception('Namespace value is not a valid URI');
            }
            self::$extensionNamespaces[$namespace] = $uri;
            return true;
        }
        return false;
    }

    /**
     * Get a specific Extension Namespace value
     *
     * @param string $namespace Namespace
     *
     * @return string|null
     */
    public static function getExtensionNamespace($namespace)
    {
        if (isset(self::$extensionNamespaces[$namespace])) {
            return self::$extensionNamespaces[$namespace];
        }
        return null;
    }

    /**
     * Get the array of Extension Namespaces for this instance
     *
     * @return array
     */
    public static function getExtensionNamespaces()
    {
        return self::$extensionNamespaces;
    }

    /**
     * Set a new Association Type based on SHA type
     *
     * @param string $hash Hash
     *
     * @throws OpenID_Exception on invalid hash
     * @return void
     */
    public static function setAssocType($hash)
    {
        $hash = strtolower($hash);
        if (!in_array($hash, array('sha1','sha256'))) {
            throw new OpenID_Exception(
                '"' . $hash . '" is an invalid hash for an Association Type'
            );
        }
        if ($hash == 'sha1') {
            self::$assocType = 'HMAC-SHA1';
            return;
        }
        self::$assocType = 'HMAC-SHA256';
    }

    /**
     * Return the current Association Type preference
     *
     * @return string
     */
    public static function getAssocType()
    {
        return self::$assocType;
    }

    /**
     * Set a new Session Type based on SHA type
     *
     * @param string $type Sha type
     *
     * @throws OpenID_Excpetion on invalid hash
     * @return void
     */
    public static function setSessionType($type)
    {
        $hash = strtolower($type);
        if (!in_array($hash, array('sha1','sha256', OpenID::OPENID_NO_ENCRYPTION))) {
            throw new OpenID_Exception(
                '"' . $type . '" is an invalid hash for a Session Type'
            );
        }
        if ($type == 'sha1') {
            self::$sessionType = 'DH-SHA1';
            return;
        } elseif ($type == 'sha256') {
            self::$sessionType = 'DH-SHA256';
        } else {
            self::$sessionType = 'no-encryption';
        }
    }

    /**
     * Return the current Session Type preference
     *
     * @return string
     */
    public static function getSessionType()
    {
        return self::$sessionType;
    }

    /**
     * Based on the current Association Type settings return a hash algorithm
     * name we may use.
     *
     * @param string $type Association Type
     *
     * @return string|boolean
     */
    public static function getHashFromAssociation($type = null)
    {
        if (is_null($type)) {
            $type = self::getAssocType();
        }
        if (preg_match("/SHA256/i", $type)) {
            return 'SHA256';
        } elseif (preg_match("/SHA1/i", $type)) {
            return 'SHA1';
        }
        return false;
    }

    /**
     * Set the client to being stateless or statefull.
     *
     * @param string $mode State mode
     *
     * @return void
     */
    public static function setMode($mode)
    {
        if ($mode == OpenID::OPENID_STATELESS) {
            self::$mode = OpenID::OPENID_STATELESS;
        } else {
            self::$mode = OpenID::OPENID_STATEFULL;
        }
    }

    /**
     * Get current stateless|full mode
     *
     * @return string
     */
    public static function getMode()
    {
        return self::$mode;
    }
}
?>
